#include "state.h"

#include <algorithm>
#include <chrono>
#include <cassert>
#include <cmath>

constexpr int BASE_PRODUCTION = 25;
constexpr int BASE_PRICE[] = { 50, 200, 1000 };
constexpr int RESEARCH_PRICE = 200;

State::State( double aTier, int aPlaythrough )
    : tier( aTier )
    , playthrough( aPlaythrough ) {
    //  buildings
    buildings.emplace_back( UpgradeType::SolarPanel, "Solar Panel", BASE_PRICE[static_cast<int>( UpgradeType::SolarPanel )] );
    buildings.emplace_back( UpgradeType::WindMill, "Wind Mill", BASE_PRICE[static_cast<int>( UpgradeType::WindMill )] );
    buildings.emplace_back( UpgradeType::Reflector, "Reflector", BASE_PRICE[static_cast<int>( UpgradeType::Reflector )], 1 );

    //  researches
    researches.emplace_back( UpgradeType::Generator_Power, "Power Generator", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Generator_XP, "XP Generator", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Generator_Asset, "Asset Generator", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Effeciency_SolarPanel, "Solar Efficiency", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Effeciency_WindWill, "Wind Mill Efficiency", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Effeciency_Reflector, "Reflector Efficiency", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Cost_Reduction_Assets, "Cost reduction", RESEARCH_PRICE );
    researches.emplace_back( UpgradeType::Cost_Reduction_Orb, "Orb cost reduction", RESEARCH_PRICE );

    // products
    products.emplace_back( ProductType::Power, "Power" );
    products.emplace_back( ProductType::XP, "XP" );
    products.emplace_back( ProductType::Asset, "Asset" );

    // priority
    updateUpgradePriority();
}

double State::getTotalProduction( bool base ) const {
    double prod = 0;
    prod += getBasicProduction( UpgradeType::SolarPanel, getType( UpgradeType::SolarPanel ).getLevel(), tier )
            * ( 1 + !base * static_cast<double>( getType( UpgradeType::Effeciency_SolarPanel ).getLevel() ) * EFFICIENCY );
    prod += getBasicProduction( UpgradeType::WindMill, getType( UpgradeType::WindMill ).getLevel(), tier )
            * ( 1 + !base * static_cast<double>( getType( UpgradeType::Effeciency_WindWill ).getLevel() ) * EFFICIENCY );
    prod += getBasicProduction( UpgradeType::Reflector, getType( UpgradeType::Reflector ).getLevel(), tier )
            * ( 1 + !base * static_cast<double>( getType( UpgradeType::Effeciency_Reflector ).getLevel() ) * EFFICIENCY );
    return prod;
}

double State::getTotalProductionType( ProductType type ) const {
    switch ( type )
    {
        case ProductType::XP:
            return getTotalProduction() * ( 1 + static_cast<double>( getType( UpgradeType::Generator_XP ).getLevel() ) * GENERATOR );
        case ProductType::Asset:
            return getTotalProduction() * ( 1 + static_cast<double>( getType( UpgradeType::Generator_Asset ).getLevel() ) * GENERATOR );
        case ProductType::Power:
            return getTotalProduction() * ( 1 + static_cast<double>( getType( UpgradeType::Generator_Power ).getLevel() ) * GENERATOR );
        default:
            return 0;
    }
}

int64_t State::upgrade( UpgradeType type, int64_t level, bool overwrite ) {
    const Upgrade & upgrade = getType( type );
    int64_t finalLevel = level + !overwrite * upgrade.getLevel();
    int64_t min = std::min( upgrade.getLevel(), finalLevel );
    int64_t max = std::max( finalLevel, upgrade.getLevel() );
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            getT( ProductType::Asset ).value -= upgradePrice( type, min, max ) * ( finalLevel < upgrade.getLevel() ? -1 : 1 );
            break;
        default:
            getT( ProductType::XP ).value -= upgradePrice( type, min, max ) * ( finalLevel < upgrade.getLevel() ? -1 : 1 );
    }
    getT( type ).upgrade( level, overwrite );
    updateUpgradePriority();
    return upgrade.getLevel();
}

std::map<UpgradeType, int64_t> State::upgradeAll( ProductType resource ) {
    assert(resource == ProductType::XP || resource == ProductType::Asset);
    const auto maxPriorityType = []( const auto & data ) {
        return std::max_element( data.begin(), data.end(), []( const auto & a, const auto & b ) { return a.getPriority() < b.getPriority(); } )->getType();
    };
    std::map<UpgradeType, int64_t> upgradeList;
    const auto & data = resource == ProductType::Asset ? buildings : researches;
    UpgradeType upType = maxPriorityType(data);
    while( getType(resource).value >= upgradePrice( upType, getStep( upType )) ){
        upgrade(upType, getStep(upType));
        upgradeList[upType] += getStep(upType);
        upType = maxPriorityType(data);
    }

    return upgradeList;
}

double State::upgradePrice( UpgradeType type, int64_t count ) const {
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            return getType( type ).upgradePrice( count ) * static_cast<double>( ( 100 - getType( UpgradeType::Cost_Reduction_Assets ).getLevel() ) ) / 100
                   * ( playthrough * playthrough + 1 );
            break;
        default:
            return getType( type ).upgradePrice( count ) * ( playthrough * playthrough + 1 );
    }
}

double State::upgradePrice( UpgradeType type, int64_t begin, int64_t end ) const {
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            return getType( type ).upgradePrice( begin, end ) * static_cast<double>( ( 100 - getType( UpgradeType::Cost_Reduction_Assets ).getLevel() ) ) / 100
                   * ( playthrough * playthrough + 1 );
            break;
        default:
            return getType( type ).upgradePrice( begin, end ) * ( playthrough * playthrough + 1 );
    }
}

const Upgrade & State::getType( UpgradeType type ) const {
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            return buildings[static_cast<size_t>( type )];
            break;
        default:
            return researches[static_cast<size_t>( type ) - 3];
    }
}

const Product & State::getType( ProductType type ) const {
    return products[static_cast<size_t>( type )];
}

void State::updateUpgradePriority() {
    // buildings
    getT( UpgradeType::SolarPanel ).setPriority( netGain( UpgradeType::SolarPanel ) );
    getT( UpgradeType::WindMill ).setPriority( netGain( UpgradeType::WindMill ) );
    getT( UpgradeType::Reflector ).setPriority( netGain( UpgradeType::Reflector ) );
    // effeciency
    getT( UpgradeType::Effeciency_SolarPanel ).setPriority( netGain( UpgradeType::Effeciency_SolarPanel ) );
    getT( UpgradeType::Effeciency_WindWill ).setPriority( netGain( UpgradeType::Effeciency_WindWill ) );
    getT( UpgradeType::Effeciency_Reflector ).setPriority( netGain( UpgradeType::Effeciency_Reflector ) );
    // generator
    getT( UpgradeType::Generator_Asset ).setPriority( netGain( UpgradeType::Generator_Asset ) );
    getT( UpgradeType::Generator_Power ).setPriority( netGain( UpgradeType::Generator_Power ) );
    getT( UpgradeType::Generator_XP ).setPriority( netGain( UpgradeType::Generator_XP ) );
    // cost reduction
    getT( UpgradeType::Cost_Reduction_Assets ).setPriority( netGain( UpgradeType::Cost_Reduction_Assets ) );
    getT( UpgradeType::Cost_Reduction_Orb ).setPriority( netGain( UpgradeType::Cost_Reduction_Orb ) );

    // change weights
    if ( autoControl )
    {
        // asset weight
        double maxXPPriority = 0;
        maxXPPriority = std::max( maxXPPriority, getType( UpgradeType::Effeciency_SolarPanel ).getPriority() );
        maxXPPriority = std::max( maxXPPriority, getType( UpgradeType::Effeciency_WindWill ).getPriority() );
        maxXPPriority = std::max( maxXPPriority, getType( UpgradeType::Effeciency_Reflector ).getPriority() );
        if ( maxXPPriority > 0 )
        {
            double maxAssetPriority = std::max_element( buildings.begin(), buildings.end(), []( const auto & a, const auto & b ) {
                                          return a.getPriority() < b.getPriority();
                                      } )->getPriority();
            getT( ProductType::Asset ).weight = maxAssetPriority / maxXPPriority;
        }

        // power minimal weight
        getT( ProductType::Power ).weight = optimalPowerWeight();
    }

    // apply weights NOTE xp factor used for cost reduction
    getT( UpgradeType::Generator_Asset )
        .setPriority( getType( UpgradeType::Generator_Asset ).getPriority() * getType( ProductType::Asset ).weight * getType( ProductType::Asset ).factor );
    getT( UpgradeType::Generator_XP ).setPriority( getType( UpgradeType::Generator_XP ).getPriority() * getType( ProductType::XP ).weight );
    getT( UpgradeType::Generator_Power ).setPriority( getType( UpgradeType::Generator_Power ).getPriority() * getType( ProductType::Power ).weight );

    auto normalize = []( std::vector<Upgrade> & data ) {
        double max = std::max_element( data.cbegin(), data.cend(), []( const auto & a, const auto & b ) { return a.getPriority() < b.getPriority(); } )->getPriority();
        if ( max > 0 )
        {
            for ( Upgrade & up : data )
            {
                up.setPriority( up.getPriority() / max );
            }
        }
    };
    normalize( buildings );
    normalize( researches );
}

int State::getStep( UpgradeType type ) const {
    switch ( type )
    {
        case UpgradeType::Cost_Reduction_Assets:
        case UpgradeType::Cost_Reduction_Orb:
            return 1;
        default:
            return step;
    }
}

int State::getStepOptimal() const {
    constexpr std::array<int,3> s {100, 10, 1};
    //  check buildins
    /// @todo add production check
    auto opt = [this, &s]( UpgradeType type ) -> int {
        double value;
        switch ( type )
        {
            case UpgradeType::SolarPanel:
            case UpgradeType::Reflector:
            case UpgradeType::WindMill:
                value = getType( ProductType::Asset ).value;
                break;
            default:
                value = getType( ProductType::XP ).value;
        }

        for ( int l : s )
        {
            if ( value > upgradePrice( type, 5 * l ) )
            {
                return l;
            }
        }
        return s.back();
    };

    int stepSum = 0;
    for ( const auto & b : buildings )
    {
        stepSum += opt( b.getType() );
    }
    for ( const auto & r : researches )
    {
        if ( r.getType() != UpgradeType::Cost_Reduction_Assets && r.getType() != UpgradeType::Cost_Reduction_Orb )
            stepSum += opt( r.getType() );
    }
    stepSum /= static_cast<int>( buildings.size() + researches.size() ) - 2;
    //  NOTE can switch to bigger step only > 60%
    for ( int l : s )
    {
        if ( stepSum > l * 0.6 )
        {
            return l;
        }
    }

    return s.back();
}

double State::getTier() const {
    return tier;
}

int State::getPlaythrough() const {
    return playthrough;
}

void State::setTier( double aTier ) {
    assert( tier > 0 );
    tier = aTier;
    updateUpgradePriority();
}

void State::setPlaythrough( int p ) {
    assert( p > 0 );
    playthrough = --p;
    updateUpgradePriority();
}

void State::setStep( int s ) {
    assert( s == 1 || s == 10 || s == 100 );
    step = s;
    updateUpgradePriority();
}

void State::tick( const std::chrono::seconds & seconds ) {
    getT( ProductType::Asset ).value += getTotalProductionType( ProductType::Asset ) / 60 * static_cast<double>( seconds.count() );
    getT( ProductType::XP ).value += getTotalProductionType( ProductType::XP ) / 60 * static_cast<double>( seconds.count() );
    getT( ProductType::Power ).value += getTotalProductionType( ProductType::Power ) / 60 * static_cast<double>( seconds.count() );
}

bool State::getAutoControl() const {
    return autoControl;
}

void State::setProductWeight( ProductType type, double weight ) {
    getT( type ).weight = weight;
    updateUpgradePriority();
}

void State::setProductFactor( ProductType type, double factor ) {
    getT( type ).factor = factor;
    updateUpgradePriority();
}

void State::setProductValue( ProductType type, double value ) {
    getT( type ).value = value;
    updateUpgradePriority();
}

void State::setAutoControl( bool control ) {
    autoControl = control;
}

std::chrono::minutes State::completeEvent( std::chrono::minutes sleep, std::chrono::minutes check ) {
    const std::chrono::minutes actingTime{ std::chrono::hours{ 24 } - sleep };
    std::chrono::minutes totalTime{ 0 };
    std::chrono::minutes lastCheck{ 0 };

    while ( getType( ProductType::Power ).value + static_cast<double>( orb.value() ) < PowerOrb::maxPower )
    {
        for ( std::chrono::minutes counter{ 0 }; counter <= actingTime; counter += check )
        {
            // NOTE there is 5 steps
            // step 1. calculate how many product was generated from last check
            tick( totalTime + counter - lastCheck );

            // step 2. check if we must stop
            if ( getType( ProductType::Power ).value + static_cast<double>( orb.value() ) > PowerOrb::maxPower )
            {
                return totalTime + counter;
            }

            // step 3. calculate new values for power weight and step
            if ( autoControl )
            {
                setStep( getStepOptimal() );
            }

            // step 4. buy all what you can buy
            upgradeAll(ProductType::XP);
            upgradeAll(ProductType::Asset);

            // step 5. remember this check as last check
            lastCheck = totalTime + counter;

            // step 6. there is no step 6!
        }
        totalTime += std::chrono::hours{ 24 };
    }

    return totalTime;
}

double State::optimalPowerWeight() const {
    return 1 / ( 1 + std::exp( std::log1p( remainingTime().count() ) / M_E + ( 0.4 - getType( ProductType::Power ).factor ) * M_E ) );
}

void State::claimOrbs( int orbs ) {
    getT( ProductType::Power ).value -= ( 1 - static_cast<double>( getType( UpgradeType::Cost_Reduction_Orb ).getLevel() ) / 100.0 ) * static_cast<double>( orb.claim( orbs ) );
}

/***********
 * PRIVATE *
 ***********/
double State::getBasicProduction( UpgradeType type, int64_t level, double tier ) {
    return tier * BASE_PRODUCTION * static_cast<double>( level ) * ( 1 << static_cast<int>( type ) );
}

std::chrono::minutes State::remainingTime() const {
    double value = getType( ProductType::Power ).value + static_cast<double>( orb.value() );
    if ( value < PowerOrb::maxPower && getTotalProduction( true ) > 0 )
    {
        return std::chrono::minutes{ static_cast<std::chrono::minutes::rep>( ( PowerOrb::maxPower - value ) / getTotalProductionType( ProductType::Power ) ) };
    }
    return std::chrono::minutes{ 0 };
}

Upgrade & State::getT( UpgradeType type ) {
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            return buildings[static_cast<size_t>( type )];
            break;
        default:
            return researches[static_cast<size_t>( type ) - 3];
    }
}

Product & State::getT( ProductType type ) {
    return products[static_cast<size_t>( type )];
}

double State::netGain( UpgradeType type ) {
    double profit = 0;
    double cost = upgradePrice( type, getStep( type ) );
    switch ( type )
    {
        case UpgradeType::SolarPanel:
        case UpgradeType::WindMill:
        case UpgradeType::Reflector:
            profit = getTotalProduction();
            if ( profit > 0 )
            {
                getT( type ).upgrade( getStep( type ) );
                profit = ( getTotalProduction() - profit ) / profit;
                getT( type ).upgrade( getType( type ).getLevel() - getStep( type ), true );
            } else
            { profit = getBasicProduction( type, 1, getTier() ); }
            break;
        case UpgradeType::Effeciency_SolarPanel:
        case UpgradeType::Effeciency_WindWill:
        case UpgradeType::Effeciency_Reflector:
            profit = getTotalProduction();
            if ( profit > 0 )
            {
                getT( type ).upgrade( getStep( type ) );
                profit = ( getTotalProduction() - profit ) / profit;
                getT( type ).upgrade( getType( type ).getLevel() - getStep( type ), true );
            }
            break;
        case UpgradeType::Generator_Asset:
            profit = getTotalProductionType( ProductType::Asset );
            if ( profit > 0 )
            {
                getT( type ).upgrade( getStep( type ) );
                profit = ( getTotalProductionType( ProductType::Asset ) - profit ) / profit;
                getT( type ).upgrade( getType( type ).getLevel() - getStep( type ), true );
            }
            break;
        case UpgradeType::Generator_XP:
            profit = getTotalProductionType( ProductType::XP );
            if ( profit > 0 )
            {
                getT( type ).upgrade( getStep( type ) );
                profit = ( getTotalProductionType( ProductType::XP ) - profit ) / profit;
                getT( type ).upgrade( getType( type ).getLevel() - getStep( type ), true );
            }
            break;
        case UpgradeType::Generator_Power:
            profit = getTotalProductionType( ProductType::Power );
            if ( profit > 0 )
            {
                getT( type ).upgrade( getStep( type ) );
                profit = ( getTotalProductionType( ProductType::Power ) - profit ) / profit;
                getT( type ).upgrade( getType( type ).getLevel() - getStep( type ), true );
            }
            break;
        case UpgradeType::Cost_Reduction_Assets:
            if ( getType( type ).getLevel() < 75 && getTotalProduction() > 0 )
            {
                profit = ( static_cast<double>( ( 100 - getType( type ).getLevel() ) ) / static_cast<double>( 100 - getType( type ).getLevel() - getStep( type ) ) - 1 )
                         * getType( ProductType::XP ).factor;
            }
            break;
        case UpgradeType::Cost_Reduction_Orb:
            if ( getType( type ).getLevel() < 75
                 && getTotalProductionType( ProductType::XP ) > upgradePrice( UpgradeType::Cost_Reduction_Orb, getType( type ).getLevel(), 75 ) / 10 )
            {
                profit = 1;
                cost = 1;
            }
            break;
        default:
            assert( false );
    }

    assert( !std::isnan( profit ) && !std::isinf( profit ) );
    return profit / cost;
}
